---
id: tcpclient
title: 创建TcpClient
---

import Tag from "@site/src/components/Tag.js";

### 定义

命名空间：TouchSocket.Sockets <br/>
程序集：[TouchSocket.dll](https://www.nuget.org/packages/TouchSocket)


## 一、说明

`TcpClient`是Tcp系客户端基类，他直接参与tcp的连接、发送、接收、处理、断开等，他的业务与服务器的`TcpSessionClient`是一一对应的。

## 二、特点

- 简单易用。
- IOCP多线程。
- 内存池支持
- 高性能
- 适配器预处理，一键式解决**分包**、**粘包**、对象解析(如HTTP，Json)等。
- 超简单的同步发送、异步发送、接收等操作。
- 基于委托、插件驱动，让每一步都能执行AOP。

## 三、产品应用场景

- 所有Tcp基础使用场景：可跨平台、跨语言使用。
- 自定义协议解析场景：可解析任意数据格式的TCP数据报文。

## 四、可配置项

<details>
<summary>可配置项</summary>
<div>

#### SetMaxPackageSize
数据包最大值（单位：byte），默认1024×1024×10。该值会在适当时间，直接作用DataHandlingAdapter.MaxPackageSize。 

#### SetRemoteIPHost
链接到的远程IPHost，支持域名。支持类型：
1. 使用IPv4，传入形如：127.0.0.1:7789的字符串即可。
2. 使用IPv6，传入形如：[\*::\*]:7789的字符串即可。
3. 使用域名，必须包含协议类型，形如：http://baidu.com或者https://baidu.com:80
3. 使用IPv6域名，必须包含协议类型，形如：http://[\*::\*]:80

#### SetClientSslOption
客户端Ssl配置，为Null时则不启用。
注意，当RemoteIPHost使用https、wss的域名时，该配置会使用系统默认配置生效。

#### SetKeepAliveValue
为Socket设置的属性。
注意：该配置仅在window平台生效。

#### SetBindIPHost
绑定端口。
- 在UdpSessionBase中表示本地监听地址
- 在TcpClient中表示固定客户端端口号。


#### UseNoDelay
设置Socket的NoDelay属性，默认false。

#### SetSendTimeout
设置发送超时时间，默认0ms，即禁用该配置。

</div>
</details>

## 五、支持插件

|  插件方法| 功能 |
| --- | --- |
| ITcpConnectingPlugin | 此时Socket实际上已经完成连接，但是并没有启动接收，然后触发。 |
| ITcpConnectedPlugin | 同意连接，且成功启动接收后触发 |
| ITcpClosingPlugin | 当客户端主动调用Close时触发 |
| ITcpClosedPlugin | 当客户端断开连接后触发 |
| ITcpReceivingPlugin | 在收到原始数据时触发，所有的数据均在ByteBlock里面。 |
| ITcpReceivedPlugin | 在收到适配器数据时触发，根据适配器类型，数据可能在ByteBlock或者IRequestInfo里面。 |
| ITcpSendingPlugin | 当即将发送数据时，调用该方法在适配器之后，接下来即会发送数据。 |

## 六、创建TcpClient

#### 6.1 简单创建

简单的处理逻辑可通过**Connected**、**Closed**、**Received**等委托直接实现。

代码如下：

```csharp showLineNumbers
var tcpClient = new TcpClient();
tcpClient.Connecting = (client, e) => { return EasyTask.CompletedTask; };//即将连接到服务器，此时已经创建socket，但是还未建立tcp
tcpClient.Connected = (client, e) => { return EasyTask.CompletedTask; };//成功连接到服务器
tcpClient.Closing = (client, e) => { return EasyTask.CompletedTask; };//即将从服务器断开连接。此处仅主动断开才有效。
tcpClient.Closed = (client, e) => { return EasyTask.CompletedTask; };//从服务器断开连接，当连接不成功时不会触发。
tcpClient.Received = (client, e) =>
{
    //从服务器收到信息。但是一般byteBlock和requestInfo会根据适配器呈现不同的值。
    var mes = e.ByteBlock.Span.ToString(Encoding.UTF8);
    tcpClient.Logger.Info($"客户端接收到信息：{mes}");
    return EasyTask.CompletedTask;
};

//载入配置
await tcpClient.SetupAsync(new TouchSocketConfig()
      .SetRemoteIPHost("127.0.0.1:7789")
      .ConfigureContainer(a =>
      {
          a.AddConsoleLogger();//添加一个日志注入
      }));

await tcpClient.ConnectAsync();//调用连接，当连接不成功时，会抛出异常。

Result result = await tcpClient.TryConnectAsync();//或者可以调用TryConnectAsync
if (result.IsSuccess())
{

}

tcpClient.Logger.Info("客户端成功连接");
```

#### 6.2 继承实现

一般继承实现的话，可以从`TcpClient`继承。如果有特殊需求，也可以从`TcpClientBase`继承。

```csharp showLineNumbers
class MyTcpClient : TcpClient
{
    protected override async Task OnTcpReceived(ReceivedDataEventArgs e)
    {
        //此处逻辑单线程处理。

        //此处处理数据，功能相当于Received委托。
        string mes =e.ByteBlock.Span.ToString(Encoding.UTF8);
        Console.WriteLine($"已接收到信息：{mes}");
        await base.OnTcpReceived(e);
    }
}
```

```csharp showLineNumbers
var tcpClient = new MyTcpClient();
//载入配置
await tcpClient.SetupAsync(new TouchSocketConfig()
     .SetRemoteIPHost("127.0.0.1:7789")
     .ConfigureContainer(a =>
     {
         a.AddConsoleLogger();//添加一个日志注入
     }));

await tcpClient.ConnectAsync();//调用连接，当连接不成功时，会抛出异常。
```


## 七、接收数据

在`TcpClient`中，接收数据的方式有很多种。多种方式可以组合使用。

### 7.1 Received委托处理

当使用TcpClient创建客户端时，内部已经定义好了一个外置委托Received，可以通过该委托直接接收数据。

```csharp showLineNumbers
var tcpClient = new TcpClient();
tcpClient.Received = (client, e) =>
{
    //从服务器收到信息
    string mes = e.ByteBlock.Span.ToString(Encoding.UTF8);
    Console.WriteLine($"接收到信息：{mes}");
    return EasyTask.CompletedTask;
};

await tcpClient.ConnectAsync("127.0.0.1:7789");
```


### 7.2 插件处理 <Tag>推荐</Tag>

按照TouchSocket的设计理念，使用插件处理数据，是一项非常简单，且高度解耦的方式。步骤如下：

（1）声明插件

插件可以先继承`PluginBase`，然后再实现需要的功能插件接口，可以按需选择泛型或者非泛型实现。

如果已经有继承类，直接实现`IPlugin`接口即可。

```csharp showLineNumbers
public class MyPlugin : PluginBase, ITcpReceivedPlugin
{
    public async Task OnTcpReceived(ITcpSession client, ReceivedDataEventArgs e)
    {
        //这里处理数据接收
        //根据适配器类型，e.ByteBlock与e.RequestInfo会呈现不同的值，具体看文档=》适配器部分。
        ByteBlock byteBlock = e.ByteBlock;
        IRequestInfo requestInfo = e.RequestInfo;

        ////表示该数据已经被本插件处理，无需再投递到其他插件。

        await e.InvokeNext();
    }
}
```

（2）创建使用插件处理的客户端

```csharp showLineNumbers
var client = new TcpClient();
await client.SetupAsync(new TouchSocketConfig()
     .SetRemoteIPHost("127.0.0.1:7789")
     .ConfigureContainer(a =>
     {
         a.AddConsoleLogger();
     })
     .ConfigurePlugins(a =>
     {
         a.Add<MyPlugin>();
     }));

await client.ConnectAsync();
```

:::danger 注意

当接收数据时，ByteBlock与RequestInfo的值会根据适配器类型不同而不同。

:::  

### 7.3 异步阻塞接收 <Tag>推荐</Tag>

异步阻塞接收，即使用await的方式接收数据。其特点是能在代码上下文中，直接获取到收到的数据。例如：

```csharp showLineNumbers
var client = new TcpClient();
await client.ConnectAsync("127.0.0.1:7789");//连接

//receiver可以复用，不需要每次接收都新建
using (var receiver = client.CreateReceiver())
{
    while (true)
    {
        //receiverResult必须释放
        using (var receiverResult = await receiver.ReadAsync(CancellationToken.None))
        {
            if (receiverResult.IsCompleted)
            {
                Console.WriteLine($"客户端已断开，信息：{receiverResult.Message}");
                //断开连接了
                return;
            }

            //如果是适配器信息，则可以直接获取receiverResult.RequestInfo;
            var requestInfo = receiverResult.RequestInfo;
            var byteBlock = receiverResult.ByteBlock;

            //从服务器收到信息。
            var mes = byteBlock.Span.ToString(Encoding.UTF8);
            client.Logger.Info($"客户端接收到信息：{mes}");
        }
    }
}
```

在异步阻塞接收时，当接收的数据不满足解析条件时，还可以缓存起来，下次一起处理。

例如：下列将演示接收字符串，当没有发现“\r\n”时，将缓存数据，直到发现重要字符。

其中，`CacheMode`与`MaxCacheSize`是启用缓存的重要属性。`byteBlock.Seek`则是将已读取的数据游标移动至指定位置。

```csharp {7-8,45} showLineNumbers
var client = new TcpClient();
await client.ConnectAsync("127.0.0.1:7789");//连接

//receiver可以复用，不需要每次接收都新建
using (var receiver = client.CreateReceiver())
{
    receiver.CacheMode = true;
    receiver.MaxCacheSize = 1024 * 1024;

    var rn = Encoding.UTF8.GetBytes("\r\n");
    while (true)
    {
        //receiverResult每次接收完必须释放
        using (var receiverResult = await receiver.ReadAsync(CancellationToken.None))
        {
            //收到的数据，此处的数据会根据适配器投递不同的数据。
            var byteBlock = receiverResult.ByteBlock;
            var requestInfo = receiverResult.RequestInfo;

            if (receiverResult.IsCompleted)
            {
                //断开连接了
                Console.WriteLine($"断开信息：{receiverResult.Message}");
                return;
            }

            //在CacheMode下，byteBlock将不可能为null

            var index = 0;
            while (true)
            {
                var r = byteBlock.Span.Slice(index).IndexOf(rn);
                if (r < 0)
                {
                    break;
                }

                var str = byteBlock.Span.Slice(index, r).ToString(Encoding.UTF8);
                Console.WriteLine(str);

                index += rn.Length;
                index += r;
            }

            byteBlock.Seek(index);
        }
    }
}
```

:::tip 提示

异步阻塞接收，在等待接收数据时，不会阻塞线程资源，所以即使大量使用，也不会影响性能。

:::  


## 八、发送数据

TcpClient已经内置了发送方法，直接调用就可以发送，如果发送失败，则会立即抛出异常。

```csharp showLineNumbers
//原生
public Task SendAsync(string id, ReadOnlyMemory<byte> memory);
public Task SendAsync(string id, IRequestInfo requestInfo);
```

:::tip 提示

框架不仅内置了字节的发送，也扩展了字符串等常见数据的发送。而且还包括了`TrySend`等不会抛出异常的发送方法。

::: 

:::caution 注意

所有的发送，框架内部实际上**只实现了异步发送**，但是为了兼容性，仍然保留了同步发送的扩展。但是强烈建议如有可能，请**务必使用异步发送来提高效率**。

:::  


## 九、断线重连

断线重连，即tcp客户端在断开服务器后，主动发起的再次连接请求。

### 9.1 触发型重连

触发型重连，依靠的是Tcp断开事件（Closed）发生时，再次尝试连接。所以，这就要求客户端在初始时，至少完成一次连接。

```csharp {8,11} showLineNumbers
var tcpClient = new TcpClient();

//载入配置
await tcpClient.SetupAsync(new TouchSocketConfig()
      .SetRemoteIPHost("127.0.0.1:7789")
      .ConfigurePlugins(a => 
      {
          a.UseTcpReconnection();
      }));

await tcpClient.ConnectAsync();//调用连接
```
:::caution 注意

**触发重连，必须满足以下几个要求：**

1. 必须完成第一次连接。
2. 必须是被动断开，如果是客户端主动调用Close、Disposed等方法主动断开的话，一般不会生效。
3. 必须有显式的断开信息，也就是说，直接拔网线的话，不会立即生效，会等tcp保活到期后再生效。

:::  


### 9.2 使用Polling轮询连接插件

使用Polling断线重连，是一种无人值守的连接方式，它不要求首次连接。

```csharp showLineNumbers
.ConfigurePlugins(a => 
{
    a.UseTcpReconnection()
    .UsePolling(TimeSpan.FromSeconds(1));
})
```

:::caution 注意

**Polling重连，必须满足以下几个要求：**

1. 必须有显式的断开信息，也就是说，直接拔网线的话，也不会立即生效，会等tcp保活到期后再生效。

:::  

:::tip 提示

UseReconnection插件，可以通过设置SetActionForCheck，自己规定检查活性的方法。默认情况下，只会检验Online属性，所以无法检验出断网等情况。如果自己控制，则可以发送心跳包，以保证在线状态。

:::  

[本文示例Demo](https://gitee.com/RRQM_Home/TouchSocket/tree/master/examples/Tcp)

